Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 9a3cda077cf345c78ab6ee88ba4e1b4737b85dd1


Parents : 42d859f
Author : Mark Qvist <mark@unsigned.io>
Date : 2025-11-01T23:33:49+01:00

Implemented workaround for Kivy/SDL bug where on_resume is not dispatched on Android

Changes

6 files changed, 698 insertions(+), 16 deletions(-)

M sbapp/main.py +31 -12

Diff

diff --git a/sbapp/Makefile b/sbapp/Makefile
index 943ac9d1..a95c0815 100644
--- a/sbapp/Makefile
+++ b/sbapp/Makefile
@@ -28,6 +28,11 @@ patchsdl:
cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a/build/bootstrap_builds/sdl2/src/main/java/org/kivy/android/PythonService.java
cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/java/org/kivy/android/PythonService.java
+ # Pach python activity
+ cp patches/PythonActivity.java .buildozer/android/platform/python-for-android/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java
+ cp patches/PythonActivity.java .buildozer/android/platform/build-arm64-v8a/build/bootstrap_builds/sdl2/src/main/java/org/kivy/android/PythonActivity.java
+ cp patches/PythonActivity.java .buildozer/android/platform/build-arm64-v8a/dists/sideband/src/main/java/org/kivy/android/PythonActivity.java
+
patchpycodec2:
patchelf --replace-needed libcodec2.so.1.2 libcodec2.so .buildozer/android/platform/build-arm64-v8a/dists/sideband/_python_bundle__arm64-v8a/_python_bundle/site-packages/pycodec2/pycodec2.so
# patchelf --replace-needed libcodec2.so.1.2 libcodec2.so .buildozer/android/platform/build-arm64-v8a/dists/sideband/_python_bundle__armeabi-v7a/_python_bundle/site-packages/pycodec2/pycodec2.so

diff --git a/sbapp/main.py b/sbapp/main.py
index 8039d645..a188bf73 100644
--- a/sbapp/main.py
+++ b/sbapp/main.py
@@ -405,6 +405,7 @@ class SidebandApp(MDApp):
self.icon = self.sideband.asset_dir+"/icon.png"
self.notification_icon = self.sideband.asset_dir+"/notification_icon.png"
+ self.resume_event_scheduler = None
self.connectivity_updater = None
self.last_map_update = 0
self.last_telemetry_received = 0
@@ -830,16 +831,18 @@ class SidebandApp(MDApp):
def on_pause(self):
if self.sideband:
RNS.log("App pausing...", RNS.LOG_DEBUG)
+
+ if RNS.vendor.platformutils.is_android():
+ if self.resume_event_scheduler == None:
+ self.resume_event_scheduler = Clock.schedule_interval(self.perform_paused_check, 0.1)
+
self.sideband.setstate("app.running", True)
self.sideband.setstate("app.foreground", False)
self.app_state = SidebandApp.PAUSED
self.sideband.should_persist_data()
- if self.conversations_view != None:
- self.conversations_view.ids.conversations_scrollview.effect_cls = ScrollEffect
- self.conversations_view.ids.conversations_scrollview.scroll = 1
-
RNS.log("App paused", RNS.LOG_DEBUG)
return True
+
else:
return True
@@ -850,17 +853,10 @@ class SidebandApp(MDApp):
self.sideband.setstate("app.foreground", True)
self.sideband.setstate("wants.clear_notifications", True)
self.app_state = SidebandApp.ACTIVE
- if self.conversations_view != None:
- self.conversations_view.ids.conversations_scrollview.effect_cls = ScrollEffect
- self.conversations_view.ids.conversations_scrollview.scroll = 1
-
- else:
- RNS.log("Conversations view did not exist", RNS.LOG_DEBUG)
def ui_update_job():
time.sleep(0.05)
- def cb(dt):
- self.perform_wake_update()
+ def cb(dt): self.perform_wake_update()
Clock.schedule_once(cb, 0.1)
threading.Thread(target=ui_update_job, daemon=True).start()
@@ -879,6 +875,29 @@ class SidebandApp(MDApp):
else:
return False
+ def perform_paused_check(self, delta_time):
+ # This workaround mitigates yet another bug in Kivy
+ # on Android, where the JNI/Python bridge now for
+ # Lord knows whatever reason fails to dispatch the
+ # onResume event from the Android app lifecycle
+ # management API. So we have to resort to this hack
+ # of scheduling a manual check that reads a patched-
+ # in property on the JNI side of the app activity,
+ # and then "manually" dispatch on_resume.
+ if self.app_state == SidebandApp.PAUSED:
+ # Oh hai, we're running, but we should really
+ # be paused? What gives? Must mean we've been
+ # woken up again, but someone forgot to inform
+ # us about that. Let's have a look, shall we...
+ activity = autoclass('org.kivy.android.PythonActivity').mActivity
+ if activity.activityPaused == False:
+ # Who would have thought, the activity was
+ # resumed! Good thing we can play event-
+ # dispatch pretend ourselves.
+ Clock.unschedule(self.resume_event_scheduler)
+ self.resume_event_scheduler = None
+ self.on_resume()
+
def perform_wake_update(self):
# This workaround mitigates a bug in Kivy on Android
# which causes the UI to turn black on app resume,

diff --git a/sbapp/patches/PythonActivity.java b/sbapp/patches/PythonActivity.java
new file mode 100644
index 00000000..fc589600
--- /dev/null
+++ b/sbapp/patches/PythonActivity.java
@@ -0,0 +1,646 @@
+package org.kivy.android;
+
+import java.io.InputStream;
+import java.io.FileWriter;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.Toast;
+import android.content.res.Resources.NotFoundException;
+
+import org.libsdl.app.SDLActivity;
+
+import org.kivy.android.launcher.Project;
+
+import org.renpy.android.ResourceManager;
+
+
+public class PythonActivity extends SDLActivity {
+ private static final String TAG = "PythonActivity";
+
+ public static PythonActivity mActivity = null;
+ public boolean activityPaused = false;
+
+ private ResourceManager resourceManager = null;
+ private Bundle mMetaData = null;
+ private PowerManager.WakeLock mWakeLock = null;
+
+ public String getAppRoot() {
+ String app_root = getFilesDir().getAbsolutePath() + "/app";
+ return app_root;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.v(TAG, "PythonActivity onCreate running");
+ resourceManager = new ResourceManager(this);
+
+ Log.v(TAG, "About to do super onCreate");
+ super.onCreate(savedInstanceState);
+ Log.v(TAG, "Did super onCreate");
+
+ this.mActivity = this;
+ this.showLoadingScreen(this.getLoadingScreen());
+
+ new UnpackFilesTask().execute(getAppRoot());
+ }
+
+ public void loadLibraries() {
+ String app_root = new String(getAppRoot());
+ File app_root_file = new File(app_root);
+ PythonUtil.loadLibraries(app_root_file,
+ new File(getApplicationInfo().nativeLibraryDir));
+ }
+
+ /**
+ * Show an error using a toast. (Only makes sense from non-UI
+ * threads.)
+ */
+ public void toastError(final String msg) {
+
+ final Activity thisActivity = this;
+
+ runOnUiThread(new Runnable () {
+ public void run() {
+ Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show();
+ }
+ });
+
+ // Wait to show the error.
+ synchronized (this) {
+ try {
+ this.wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ private class UnpackFilesTask extends AsyncTask<String, Void, String> {
+ @Override
+ protected String doInBackground(String... params) {
+ File app_root_file = new File(params[0]);
+ Log.v(TAG, "Ready to unpack");
+ PythonUtil.unpackAsset(mActivity, "private", app_root_file, true);
+ PythonUtil.unpackPyBundle(mActivity, getApplicationInfo().nativeLibraryDir + "/" + "libpybundle", app_root_file, false);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(String result) {
+ // Figure out the directory where the game is. If the game was
+ // given to us via an intent, then we use the scheme-specific
+ // part of that intent to determine the file to launch. We
+ // also use the android.txt file to determine the orientation.
+ //
+ // Otherwise, we use the public data, if we have it, or the
+ // private data if we do not.
+ mActivity.finishLoad();
+
+ // finishLoad called setContentView with the SDL view, which
+ // removed the loading screen. However, we still need it to
+ // show until the app is ready to render, so pop it back up
+ // on top of the SDL view.
+ mActivity.showLoadingScreen(getLoadingScreen());
+
+ String app_root_dir = getAppRoot();
+ if (getIntent() != null && getIntent().getAction() != null &&
+ getIntent().getAction().equals("org.kivy.LAUNCH")) {
+ File path = new File(getIntent().getData().getSchemeSpecificPart());
+
+ Project p = Project.scanDirectory(path);
+ String entry_point = getEntryPoint(p.dir);
+ SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point);
+ SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir);
+ SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir);
+
+ if (p != null) {
+ if (p.landscape) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+ }
+
+ // Let old apps know they started.
+ try {
+ FileWriter f = new FileWriter(new File(path, ".launch"));
+ f.write("started");
+ f.close();
+ } catch (IOException e) {
+ // pass
+ }
+ } else {
+ String entry_point = getEntryPoint(app_root_dir);
+ SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point);
+ SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir);
+ SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir);
+ }
+
+ String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
+ Log.v(TAG, "Setting env vars for start.c and Python to use");
+ SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory);
+ SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir);
+ SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir);
+ SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib");
+ SDLActivity.nativeSetenv("PYTHONOPTIMIZE", "2");
+
+ try {
+ Log.v(TAG, "Access to our meta-data...");
+ mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo(
+ mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData;
+
+ PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
+ if ( mActivity.mMetaData.getInt("wakelock") == 1 ) {
+ mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On");
+ mActivity.mWakeLock.acquire();
+ }
+ if ( mActivity.mMetaData.getInt("surface.transparent") != 0 ) {
+ Log.v(TAG, "Surface will be transparent.");
+ getSurface().setZOrderOnTop(true);
+ getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT);
+ } else {
+ Log.i(TAG, "Surface will NOT be transparent");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+
+ // Launch app if that hasn't been done yet:
+ if (mActivity.mHasFocus && (
+ // never went into proper resume state:
+ mActivity.mCurrentNativeState == NativeState.INIT ||
+ (
+ // resumed earlier but wasn't ready yet
+ mActivity.mCurrentNativeState == NativeState.RESUMED &&
+ mActivity.mSDLThread == null
+ ))) {
+ // Because sometimes the app will get stuck here and never
+ // actually run, ensure that it gets launched if we're active:
+ mActivity.resumeNativeThread();
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ }
+ }
+
+ public static ViewGroup getLayout() {
+ return mLayout;
+ }
+
+ public static SurfaceView getSurface() {
+ return mSurface;
+ }
+
+ //----------------------------------------------------------------------------
+ // Listener interface for onNewIntent
+ //
+
+ public interface NewIntentListener {
+ void onNewIntent(Intent intent);
+ }
+
+ private List<NewIntentListener> newIntentListeners = null;
+
+ public void registerNewIntentListener(NewIntentListener listener) {
+ if ( this.newIntentListeners == null )
+ this.newIntentListeners = Collections.synchronizedList(new ArrayList<NewIntentListener>());
+ this.newIntentListeners.add(listener);
+ }
+
+ public void unregisterNewIntentListener(NewIntentListener listener) {
+ if ( this.newIntentListeners == null )
+ return;
+ this.newIntentListeners.remove(listener);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if ( this.newIntentListeners == null )
+ return;
+ this.onResume();
+ synchronized ( this.newIntentListeners ) {
+ Iterator<NewIntentListener> iterator = this.newIntentListeners.iterator();
+ while ( iterator.hasNext() ) {
+ (iterator.next()).onNewIntent(intent);
+ }
+ }
+ }
+
+ //----------------------------------------------------------------------------
+ // Listener interface for onActivityResult
+ //
+
+ public interface ActivityResultListener {
+ void onActivityResult(int requestCode, int resultCode, Intent data);
+ }
+
+ private List<ActivityResultListener> activityResultListeners = null;
+
+ public void registerActivityResultListener(ActivityResultListener listener) {
+ if ( this.activityResultListeners == null )
+ this.activityResultListeners = Collections.synchronizedList(new ArrayList<ActivityResultListener>());
+ this.activityResultListeners.add(listener);
+ }
+
+ public void unregisterActivityResultListener(ActivityResultListener listener) {
+ if ( this.activityResultListeners == null )
+ return;
+ this.activityResultListeners.remove(listener);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if ( this.activityResultListeners == null )
+ return;
+ this.onResume();
+ synchronized ( this.activityResultListeners ) {
+ Iterator<ActivityResultListener> iterator = this.activityResultListeners.iterator();
+ while ( iterator.hasNext() )
+ (iterator.next()).onActivityResult(requestCode, resultCode, intent);
+ }
+ }
+
+ public static void start_service(
+ String serviceTitle,
+ String serviceDescription,
+ String pythonServiceArgument
+ ) {
+ _do_start_service(
+ serviceTitle, serviceDescription, pythonServiceArgument, true
+ );
+ }
+
+ public static void start_service_not_as_foreground(
+ String serviceTitle,
+ String serviceDescription,
+ String pythonServiceArgument
+ ) {
+ _do_start_service(
+ serviceTitle, serviceDescription, pythonServiceArgument, false
+ );
+ }
+
+ public static void _do_start_service(
+ String serviceTitle,
+ String serviceDescription,
+ String pythonServiceArgument,
+ boolean showForegroundNotification
+ ) {
+ Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
+ String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath();
+ String app_root_dir = PythonActivity.mActivity.getAppRoot();
+ String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service");
+ serviceIntent.putExtra("androidPrivate", argument);
+ serviceIntent.putExtra("androidArgument", app_root_dir);
+ serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point);
+ serviceIntent.putExtra("pythonName", "python");
+ serviceIntent.putExtra("pythonHome", app_root_dir);
+ serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib");
+ serviceIntent.putExtra("serviceStartAsForeground",
+ (showForegroundNotification ? "true" : "false")
+ );
+ serviceIntent.putExtra("serviceTitle", serviceTitle);
+ serviceIntent.putExtra("serviceDescription", serviceDescription);
+ serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument);
+ PythonActivity.mActivity.startService(serviceIntent);
+ }
+
+ public static void stop_service() {
+ Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class);
+ PythonActivity.mActivity.stopService(serviceIntent);
+ }
+
+ /** Loading screen view **/
+ public static ImageView mImageView = null;
+ public static View mLottieView = null;
+ /** Whether main routine/actual app has started yet **/
+ protected boolean mAppConfirmedActive = false;
+ /** Timer for delayed loading screen removal. **/
+ protected Timer loadingScreenRemovalTimer = null;
+
+ // Overridden since it's called often, to check whether to remove the
+ // loading screen:
+ @Override
+ protected boolean sendCommand(int command, Object data) {
+ boolean result = super.sendCommand(command, data);
+ considerLoadingScreenRemoval();
+ return result;
+ }
+
+ /** Confirm that the app's main routine has been launched.
+ **/
+ @Override
+ public void appConfirmedActive() {
+ if (!mAppConfirmedActive) {
+ Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal");
+ mAppConfirmedActive = true;
+ considerLoadingScreenRemoval();
+ }
+ }
+
+ /** This is called from various places to check whether the app's main
+ * routine has been launched already, and if it has, then the loading
+ * screen will be removed.
+ **/
+ public void considerLoadingScreenRemoval() {
+ if (loadingScreenRemovalTimer != null)
+ return;
+ runOnUiThread(new Runnable() {
+ public void run() {
+ if (((PythonActivity)PythonActivity.mSingleton).mAppConfirmedActive &&
+ loadingScreenRemovalTimer == null) {
+ // Remove loading screen but with a delay.
+ // (app can use p4a's android.loadingscreen module to
+ // do it quicker if it wants to)
+ // get a handler (call from main thread)
+ // this will run when timer elapses
+ TimerTask removalTask = new TimerTask() {
+ @Override
+ public void run() {
+ // post a runnable to the handler
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ PythonActivity activity =
+ ((PythonActivity)PythonActivity.mSingleton);
+ if (activity != null)
+ activity.removeLoadingScreen();
+ }
+ });
+ }
+ };
+ loadingScreenRemovalTimer = new Timer();
+ loadingScreenRemovalTimer.schedule(removalTask, 5000);
+ }
+ }
+ });
+ }
+
+ public void removeLoadingScreen() {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ View view = mLottieView != null ? mLottieView : mImageView;
+ if (view != null && view.getParent() != null) {
+ ((ViewGroup)view.getParent()).removeView(view);
+ mLottieView = null;
+ mImageView = null;
+ }
+ }
+ });
+ }
+
+ public String getEntryPoint(String search_dir) {
+ /* Get the main file (.pyc|.py) depending on if we
+ * have a compiled version or not.
+ */
+ List<String> entryPoints = new ArrayList<String>();
+ entryPoints.add("main.pyc"); // python 3 compiled files
+ for (String value : entryPoints) {
+ File mainFile = new File(search_dir + "/" + value);
+ if (mainFile.exists()) {
+ return value;
+ }
+ }
+ return "main.py";
+ }
+
+ protected void showLoadingScreen(View view) {
+ try {
+ if (mLayout == null) {
+ setContentView(view);
+ } else if (view.getParent() == null) {
+ mLayout.addView(view);
+ }
+ } catch (IllegalStateException e) {
+ // The loading screen can be attempted to be applied twice if app
+ // is tabbed in/out, quickly.
+ // (Gives error "The specified child already has a parent.
+ // You must call removeView() on the child's parent first.")
+ }
+ }
+
+ protected void setBackgroundColor(View view) {
+ /*
+ * Set the presplash loading screen background color
+ * https://developer.android.com/reference/android/graphics/Color.html
+ * Parse the color string, and return the corresponding color-int.
+ * If the string cannot be parsed, throws an IllegalArgumentException exception.
+ * Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
+ * 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow',
+ * 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia',
+ * 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'.
+ */
+ String backgroundColor = resourceManager.getString("presplash_color");
+ if (backgroundColor != null) {
+ try {
+ view.setBackgroundColor(Color.parseColor(backgroundColor));
+ } catch (IllegalArgumentException e) {}
+ }
+ }
+
+ protected View getLoadingScreen() {
+ // If we have an mLottieView or mImageView already, then do
+ // nothing because it will have already been made the content
+ // view or added to the layout.
+ if (mLottieView != null || mImageView != null) {
+ // we already have a splash screen
+ return mLottieView != null ? mLottieView : mImageView;
+ }
+
+ // first try to load the lottie one
+ try {
+ mLottieView = getLayoutInflater().inflate(
+ this.resourceManager.getIdentifier("lottie", "layout"),
+ mLayout,
+ false
+ );
+ try {
+ if (mLayout == null) {
+ setContentView(mLottieView);
+ } else if (PythonActivity.mLottieView.getParent() == null) {
+ mLayout.addView(mLottieView);
+ }
+ } catch (IllegalStateException e) {
+ // The loading screen can be attempted to be applied twice if app
+ // is tabbed in/out, quickly.
+ // (Gives error "The specified child already has a parent.
+ // You must call removeView() on the child's parent first.")
+ }
+ setBackgroundColor(mLottieView);
+ return mLottieView;
+ }
+ catch (NotFoundException e) {
+ Log.v("SDL", "couldn't find lottie layout or animation, trying static splash");
+ }
+
+ // no lottie asset, try to load the static image then
+ int presplashId = this.resourceManager.getIdentifier("presplash", "drawable");
+ InputStream is = this.getResources().openRawResource(presplashId);
+ Bitmap bitmap = null;
+ try {
+ bitmap = BitmapFactory.decodeStream(is);
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {};
+ }
+
+ mImageView = new ImageView(this);
+ mImageView.setImageBitmap(bitmap);
+ setBackgroundColor(mImageView);
+
+ mImageView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.FILL_PARENT));
+ mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ return mImageView;
+ }
+
+ @Override
+ protected void onPause() {
+ if (this.mWakeLock != null && mWakeLock.isHeld()) {
+ this.mWakeLock.release();
+ }
+
+ this.activityPaused = true;
+ Log.v(TAG, "onPause()");
+ try {
+ super.onPause();
+ } catch (UnsatisfiedLinkError e) {
+ // Catch pause while still in loading screen failing to
+ // call native function (since it's not yet loaded)
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ if (this.mWakeLock != null) {
+ this.mWakeLock.acquire();
+ }
+ this.activityPaused = false;
+ Log.v(TAG, "onResume() in PythonActivity");
+ try {
+ super.onResume();
+ } catch (UnsatisfiedLinkError e) {
+ // Catch resume while still in loading screen failing to
+ // call native function (since it's not yet loaded)
+ }
+ considerLoadingScreenRemoval();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ try {
+ super.onWindowFocusChanged(hasFocus);
+ } catch (UnsatisfiedLinkError e) {
+ // Catch window focus while still in loading screen failing to
+ // call native function (since it's not yet loaded)
+ }
+ considerLoadingScreenRemoval();
+ }
+
+ /**
+ * Used by android.permissions p4a module to register a call back after
+ * requesting runtime permissions
+ **/
+ public interface PermissionsCallback {
+ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
+ }
+
+ private PermissionsCallback permissionCallback;
+ private boolean havePermissionsCallback = false;
+
+ public void addPermissionsCallback(PermissionsCallback callback) {
+ permissionCallback = callback;
+ havePermissionsCallback = true;
+ Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult");
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ Log.v(TAG, "onRequestPermissionsResult()");
+ if (havePermissionsCallback) {
+ Log.v(TAG, "onRequestPermissionsResult passed to callback");
+ permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ /**
+ * Used by android.permissions p4a module to check a permission
+ **/
+ public boolean checkCurrentPermission(String permission) {
+ if (android.os.Build.VERSION.SDK_INT < 23)
+ return true;
+
+ try {
+ java.lang.reflect.Method methodCheckPermission =
+ Activity.class.getMethod("checkSelfPermission", String.class);
+ Object resultObj = methodCheckPermission.invoke(this, permission);
+ int result = Integer.parseInt(resultObj.toString());
+ if (result == PackageManager.PERMISSION_GRANTED)
+ return true;
+ } catch (IllegalAccessException | NoSuchMethodException |
+ InvocationTargetException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Used by android.permissions p4a module to request runtime permissions
+ **/
+ public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) {
+ if (android.os.Build.VERSION.SDK_INT < 23)
+ return;
+ try {
+ java.lang.reflect.Method methodRequestPermission =
+ Activity.class.getMethod("requestPermissions",
+ String[].class, int.class);
+ methodRequestPermission.invoke(this, permissions, requestCode);
+ } catch (IllegalAccessException | NoSuchMethodException |
+ InvocationTargetException e) {
+ }
+ }
+
+ public void requestPermissions(String[] permissions) {
+ requestPermissionsWithRequestCode(permissions, 1);
+ }
+
+ public static void changeKeyboard(int inputType) {
+ if (SDLActivity.keyboardInputType != inputType){
+ SDLActivity.keyboardInputType = inputType;
+ InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.restartInput(mTextEdit);
+ }
+ }
+}

diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py
index 725c2979..5aa22acb 100644
--- a/sbapp/sideband/core.py
+++ b/sbapp/sideband/core.py
@@ -4204,6 +4204,7 @@ class SidebandCore():
self.interface_tcp = None
self.interface_i2p = None
self.interface_rnode = None
+ self.interface_weave = None
self.interface_modem = None
self.interface_serial = None
@@ -4326,7 +4327,7 @@ class SidebandCore():
self.setstate("init.loadingstate", "Starting RNode")
self.__add_rnodeinterface()
- if self.config["connect_weave"] or True:
+ if self.config["connect_weave"]:
self.setstate("init.loadingstate", "Starting Weave")
self.__add_weaveinterface()

diff --git a/sbapp/ui/conversations.py b/sbapp/ui/conversations.py
index e2491fc3..8dd7a214 100644
--- a/sbapp/ui/conversations.py
+++ b/sbapp/ui/conversations.py
@@ -489,6 +489,7 @@ Builder.load_string("""
icon: "dots-vertical"
<ConversationList>:
+ id: conversations_scrollview
viewclass: "ConversationEntry"
effect_cls: "ScrollEffect"

diff --git a/sbapp/ui/layouts.py b/sbapp/ui/layouts.py
index f144eb59..9f55656f 100644
--- a/sbapp/ui/layouts.py
+++ b/sbapp/ui/layouts.py
@@ -163,9 +163,9 @@ MDNavigationLayout:
OneLineIconListItem:
- text: "Encryption Keys"
+ text: "Backup & Keys"
on_release: root.ids.screen_manager.app.keys_action(self)
-
+
IconLeftWidget:
icon: "key-chain"
on_release: root.ids.screen_manager.app.keys_action(self)
@@ -1131,7 +1131,7 @@ MDScreen:
orientation: "vertical"
MDTopAppBar:
- title: "Encryption Keys"
+ title: "Backup & Keys"
anchor_title: "left"
elevation: 0
left_action_items:
@@ -1160,6 +1160,16 @@ MDScreen:
text_size: self.width, None
height: self.texture_size[1]
+ MDRectangleFlatIconButton:
+ id: keys_backup
+ icon: "home-export-outline"
+ text: "Backup Sideband Profile"
+ padding: [dp(0), dp(14), dp(0), dp(14)]
+ icon_size: dp(24)
+ font_size: dp(16)
+ size_hint: [1.0, None]
+ on_release: root.app.profile_backup_action(self)
+
MDRectangleFlatIconButton:
id: keys_display
icon: "eye"


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────